package cn.zzdt4j.auth.filter;

import cn.hutool.crypto.symmetric.AES;
import cn.hutool.json.JSONUtil;
import cn.zzdt4j.auth.model.biz.user.JwtUser;
import cn.zzdt4j.auth.model.biz.user.LoginUser;
import cn.zzdt4j.auth.toolkit.AESUtil;
import cn.zzdt4j.auth.toolkit.JwtTokenUtil;
import cn.zzdt4j.auth.toolkit.ReturnT;
import cn.zzdt4j.server.common.base.Results;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.codec.DecodingException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static cn.zzdt4j.auth.constant.Constants.SPLIT_COMMA;
import static cn.zzdt4j.common.constant.Constants.BASE_PATH;
import static cn.zzdt4j.common.constant.Constants.MAP_INITIAL_CAPACITY;

/**
 * JWT authentication filter.
 *
 * @author by <a href="mailto:ligang941012@gmail.com">gang.Li</a>
 * @since 2023/10/28 22:17
 */
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final ThreadLocal<Integer> rememberMe = new ThreadLocal();
    private UserDetailsService userDetailsService;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl(BASE_PATH + "/auth/login");
    }

    public void setLdapUserDetailsService(UserDetailsService userDetailsServiceImpl) {
        this.userDetailsService = userDetailsServiceImpl;
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        Authentication authentication = null;
        try {
            LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
            String password = AESUtil.decrypt(loginUser.getPassword(), loginUser.getTag());
            loginUser.setPassword(password);

            request.setAttribute("loginUser", loginUser);
            rememberMe.set(loginUser.getRememberMe());
            final UserDetails userDetails = userDetailsService.loadUserByUsername(loginUser.getUsername());
            authentication = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        } catch (GeneralSecurityException e) {
            log.warn("Password decode exception: {}", e.getMessage());
            throw new DecodingException(e.getMessage());
        } catch (UsernameNotFoundException e) {
            log.warn("User {} not found", e.getMessage());
            throw e;
        } catch (BadCredentialsException e) {
            log.warn("Bad credentials exception: {}", e.getMessage());
            throw e;
        } catch (Exception e) {
            log.error("Attempt authentication error.", e);
        }
        return authentication;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException {
        try {
            JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
            boolean isRemember = rememberMe.get() == 1;
            String role = "";
            Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                role = authority.getAuthority();
            }
            String token = JwtTokenUtil.createToken(jwtUser.getId(), jwtUser.getUsername(), role, isRemember);
            response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
            response.setCharacterEncoding("UTF-8");
            Map<String, Object> maps = new HashMap<>(MAP_INITIAL_CAPACITY);
            maps.put("data", JwtTokenUtil.TOKEN_PREFIX + token);
            maps.put("roles", role.split(SPLIT_COMMA));
            response.getWriter().write(JSONUtil.toJsonStr(Results.success(maps)));
        } finally {
            rememberMe.remove();
        }
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(new ReturnT(ReturnT.JWT_FAIL_CODE, getMessage(failed))));
    }

    /**
     * Return different echo information to the front end according to different exception types
     */
    private String getMessage(AuthenticationException failed) {
        String message = "Server Error";
        if (failed instanceof UsernameNotFoundException) {
            message = "用户不存在";
        } else if (failed instanceof BadCredentialsException) {
            message = "密码错误";
        }
        return message;
    }
}
